﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;

namespace Perceiveit.Xml
{

    /// <summary>
    /// The XList encapsulates a mutable generic list of inner TItems based upon its the Xml data
    /// </summary>
    /// <typeparam name="TItem">The set of XEntity or its subclasses this list contains</typeparam>
    /// <remarks>if the list is to contain a single type (no subclasses) then use the XSimpleList as a base class</remarks>
    public class XList<TItem> : XEntityCollection<TItem>, 
                                IList<TItem> 
        where TItem : XEntity
    {

        //
        // public ctor
        //


        #region public XList(XFactory<TItem> factory)

        /// <summary>
        /// Creates a new XList collection uusing the specified XFactory 
        /// to instantiate any existing elements that should be in the list
        /// </summary>
        /// <param name="factory">The factory that will be used to create inner elements - see also XSimpleFactory and XChoiceFactory</param>
        public XList(XFactory<TItem> factory)
            : base(factory)
        {
        }

        #endregion

        //
        // public properties
        //

        #region public override bool IsReadOnly {get;}

        /// <summary>
        /// Gets a value indicating whether this list is readonly. The default for a list is false
        /// </summary>
        public override bool IsReadOnly
        {
            get { return false; }
        }

        #endregion


        #region public TItem this[int index] {get;set;}

        /// <summary>
        /// Gets or sets the element at the specified index
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        public override TItem this[int index]
        {
            get { return this.GetItemAt(index); }
            set 
            {
                if (null == value)
                    throw new NullReferenceException("Cannot set a value to null in the list");

                TItem prev = this.GetItemAt(index);

                this.RemoveItemAtIndex(prev, index);
                this.InsertItemAtIndex(index, value);
            }
        }
        

        #endregion

        //
        // public methods
        //

        #region public int Add(TItem item)

        /// <summary>
        /// Adds the specified item to the end of the list
        /// </summary>
        /// <param name="item">The item to add</param>
        /// <returns>The new index of the item</returns>
        public int Add(TItem item)
        {
            if(null == item)
                throw new ArgumentNullException("item");

            int index = this.Count;
            this.InsertItemAtIndex(index, item);

            return index;
        }

        #endregion


        #region public bool Remove(TItem item)

        /// <summary>
        /// Removes the first occurance of the specified item from the list
        /// </summary>
        /// <param name="item">The item to remove</param>
        /// <returns>true if the item was found and removed</returns>
        public bool Remove(TItem item)
        {
            if (null == item)
                return false;

            int index = this.Cache.IndexOf(item);
            if (index >= 0)
            {
                return RemoveItemAtIndex(item, index);
            }
            else
                return false;
        }

        #endregion


        #region public void RemoveAt(int index)

        /// <summary>
        /// Removes the item at the specified index from the list
        /// </summary>
        /// <param name="index">The zero-based index of the element to remove</param>
        public void RemoveAt(int index)
        {
            if (index < 0 || index >= this.Count)
                throw new ArgumentOutOfRangeException("index");

            TItem item = this.GetItemAt(index);
            if (null != item)
            {
                this.RemoveItemAtIndex(item, index);
            }
        }

        #endregion


        #region public void Insert(int index, TItem item)

        /// <summary>
        /// Inserts the item into the list at the specified index
        /// </summary>
        /// <param name="index">The zero-based index at which the item should be inserted</param>
        /// <param name="item">The item to insert. The item CANNOT be null</param>
        public void Insert(int index, TItem item)
        {
            InsertItemAtIndex(index, item);
        }

        

        #endregion


        #region public void Clear()

        /// <summary>
        /// Removes all elements from the list
        /// </summary>
        public void Clear()
        {
            this.ClearItems();
        }

        #endregion


        #region public bool Contains(TItem item)

        /// <summary>
        /// Checks if the list contains the specified item, 
        /// returning true if it does.
        /// </summary>
        /// <param name="item">The item to check</param>
        /// <returns>True if the list contains this item</returns>
        public bool Contains(TItem item)
        {
            return this.Cache.Contains(item);
        }

        #endregion


        #region public int IndexOf(TItem item)

        /// <summary>
        /// Searchs for the specified object and returns the zero-based index of the first occurence within the entire list 
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        public int IndexOf(TItem item)
        {
            return this.Cache.IndexOf(item);
        }

        #endregion


        #region public void CopyTo(TItem[] array, int arrayIndex)

        /// <summary>
        /// Adds all the XEntities in this list to the provided array starting at the specified index
        /// </summary>
        /// <param name="array">The array to copy the items to</param>
        /// <param name="arrayIndex">The index specifying the position in the array 
        /// where the copied items will start</param>
        public void CopyTo(TItem[] array, int arrayIndex)
        {
            if (null == array)
                throw new ArgumentNullException("array");

            if (array.Length < arrayIndex + this.Count)
                throw new ArgumentOutOfRangeException("arrayIndex");

            for (int i = 0; i < this.Count; i++)
            {
                TItem item = this.GetItemAt(i);
                array[i + arrayIndex] = item;
            }
        }

        #endregion


        //
        // explicit interface implmentations
        //

        #region void ICollection<TItem>.Add(TItem item)

        /// <summary>
        /// It's useful to return the index of an item that was added so we
        /// provide this as default and implment the ICollection&lt;&gt.Add method
        /// explicitly here.
        /// </summary>
        /// <param name="item"></param>
        void ICollection<TItem>.Add(TItem item)
        {
            this.Add(item);
        }

        #endregion


        //
        // protected implementation
        //


        /// <summary>
        /// Performs a complete clear of the list.
        /// Inheritors can override this method to add their own functionality
        /// </summary>
        protected virtual void ClearItems()
        {
            if (this.Count == 0)
                return;

            //We remove the children from the underlying xml
            //any existing object references will still refernce the correct xml, but it
            //is not attached to the main document
            try
            {
                foreach (XElement ele in this.KnownChildren)
                {
                    ele.Remove();
                }
                this.RegisterCollectionChanged();
                this.Cache.Clear();
                this.KnownChildren.Clear();

            }
            catch (Exception ex)
            {
                //we can't efficiently roll back from a clear so throw a new exception
                throw new OperationCanceledException("Clear failed, and the collection is in an indeterminant state. Please reload or discard", ex);
            }
        }

        /// <summary>
        /// Performs the actual insert of an item into this list. 
        /// Inheritors can override this method to add their own functionality
        /// </summary>
        /// <param name="index"></param>
        /// <param name="item"></param>
        protected virtual void InsertItemAtIndex(int index, TItem item)
        {
            if (null == item)
                throw new ArgumentNullException("item");

            TItem last;

            if (index == 0)
                last = null;
            else if (index <= Count)
                last = this.GetItemAt(index - 1);
            else
                throw new ArgumentOutOfRangeException("index");

            
            if (!item.IsInitialized)
            {
                //if this item is not initialized then but does have a node,
                //then it does not have the correct name - need to import the child
                //nodes against the correct name then initialize it
                if (item.HasExistingNode)
                {
                    XName name = this.Factory.GetNameForEntity(item);
                    XElement ele = new XElement(name, item.Element.Nodes());
                    if (item.Element.HasAttributes)
                        ele.Add(item.Element.Attributes());
                    item.Init(ele);
                }
            }

            //attempt to perform the insert atomically
            bool insertedinCache = false;
            bool insertedInKnown = false;

            try
            {
                this.InsertCachedItem(index, item);
                insertedinCache = true;

                this.KnownChildren.Insert(index, item.Element);
                insertedInKnown = true;

                if (last != null)
                    last.Element.AddAfterSelf(item.Element);
                else
                    this.Element.AddFirst(item.Element);

                this.RegisterCollectionChanged();
            }
            catch
            {
                //attempt to roll back and then rethrow the error
                try
                {
                    if (insertedInKnown)
                        this.KnownChildren.RemoveAt(index);
                    if (insertedinCache)
                        this.RemoveCachedItemAt(index);
                }
                catch { }

                throw;
            }
        }


        /// <summary>
        /// Removes the specified item (which is at the specified index) from the list.
        /// Inheritors can override this method to add their own functionality
        /// </summary>
        /// <param name="item"></param>
        /// <param name="index"></param>
        /// <returns></returns>
        protected virtual bool RemoveItemAtIndex(TItem item, int index)
        {
            bool removedfromCache = false;
            bool removedfromKnown = false;

            try
            {
                this.Cache.RemoveAt(index);
                removedfromCache = true;
                removedfromKnown = this.KnownChildren.Remove(item.Element);
                this.RegisterCollectionChanged();
                item.Element.Remove();
            }
            catch
            {
                try
                {
                    if (removedfromKnown)
                        this.KnownChildren.Insert(index, item.Element);
                    if (removedfromCache)
                        this.Cache.Insert(index, item);
                }
                catch { }
                //re-throw the first exception
                throw;
            }
            return true;
        }



    }

    public class XListFactory<T> : XFactory<XList<T>> where T : XEntity
    {
        private XName _element;
        private XFactory<T> _factory;

        public XListFactory(XFactory<T> childfactory, XName elementname)
            : base()
        {
            this._element = elementname;
            this._factory = childfactory;
        }

        public override bool IsKnownName(XName name, out XFactory<XList<T>> factory)
        {
            if (_element == name)
            {
                factory = this;
                return true;
            }
            else
            {
                factory = null ;
                return false;
            }
        }

        public override bool IsKnownEntity(XList<T> entity, out XFactory<XList<T>> factory)
        {
            factory = this;
            return true;
        }

        public override XList<T> GetEntityForName(XName name)
        {
            return new XList<T>(this._factory);
        }

        public override XName GetNameForEntity(XList<T> entity)
        {
            return this._element;
        }
    }

    public class XPropertyList<T> : XPropertyComplex<XList<T>> where T: XEntity
    {

        public XList<T> Create()
        {
            return this.Factory.GetEntityForName(this.Name);
        }

        public XPropertyList(XFactory<XList<T>> listFactory)
            : base(listFactory)
        {
        }
    }
}
